
CircleCI × Contentful × S3で作るJamstackなブログ環境。
こんにちは、ベルリンオフィスの小西です。
最近日本語の記事も充実してきたJamstack。説明はこちらの記事で書かれているので省略しますが、要はサイト表示が高速でセキュリティも強くてスケーラビリティもある環境構成のことです。
今回は爆速にページを表示でき、記事データの管理も楽で、かつセキュリティもあまり気にしなくていいブログサービスを構築したいと思います。
目次
- 環境構成
- 前提
- 1. Contentfulの準備
- 2. Gatsbyアプリの立ち上げ
- 3. S3へのアップロード
- 4. CircleCIのセットアップ
- 5. ContentfulとCircleCIの連携
- さいごに
記事が長めで初歩的なことも書いてたりするので、適宜必要な見出しまで飛んでってください。
ローカルでアプリ立ち上げたいだけなら1と2だけ、既にContentfulアプリがあってCircleCIと連携したい人は5だけ読んでいただければ。
環境構成
一つのサービスがボトルネックにならないよう、マイクロサービスの集合をイメージしています。
Contentful
ヘッドレスCMSとして、今回作るブログの記事を入稿/管理します。
※ヘッドレスCMSとは
管理画面だけを提供してくれるCMSで、APIでコンテンツを取得する前提で作られており、Jamstack構成と相性がいいです。
Gatsby.js
jamstack.orgでもフレームワークとして真っ先に挙げられている静的サイト生成フレームワーク。
CircleCI
Github上のソースコードの変更、もしくはContentful上での記事更新をトリガーとして自動でビルドとデプロイを行うようします。
S3
サイトのホスティングを行います。今回の構成ではここだけお金がかかります。
Github
説明不要。ソースコードの保管場所として利用します。
前提
- node.jsをインストール済み
- AWSアカウントを作成済み
- Githubアカウントを作成済み
1. Contentfulの準備
1-1. www.contentful.com に登録する
ある程度の規模のブログであれば無料で運用できてしまうのがContentfulのいいところ。
1-2. Spaceを作成する
ContentfulはSpaceという単位でアプリを区切ります。
今回はTest AppというSpaceを作ってみました。

1-3. Content modelを作成する
Conetnt modelとは「記事に入力する項目の設定」です。
今回は、記事のタイトル、URLスラッグ、サムネイル画像、本文の4項目を投稿するだけの簡単なブログを作ってみます。
Contentfulの「Content model」から「Add content type」をクリックします。
APIで記事を取得する際、コンテンツモデルごとのクエリを書くので、コンテンツモデルを特定するAPI IDはblogArticleとしておきます。

1-4. 入力フィールドの作成
まだblogArticleの中身が空っぽなので、「Add field」から追加していきます。

次の4つのフィールドを作成します。
- Title(Field ID: title): 記事のタイトルです。「Text」タイプを選択し、作成時、「This field represents the Entry title」にチェックを入れ、かつValidationから「Required field」にチェックを入れてください。
- Slug(Field ID: slug): 記事URLのSlugです。「Text」タイプを選択し、作成時、Validationから「Required field」「Unique field」にチェックを入れ、Appearanceから「URL」を選択します。
- Thumbnail(Field ID: thumbnail): サムネイル画像です。「Media」タイプを選択します。
- Content(Field ID: content): 記事の本文です。「Rich Text」タイプを選択します。
最終的にこんなフィールドができていればOK。

これで記事を投稿する準備ができましたので、保存します。
1-4. テスト記事の投稿
ヘッダーの「Content」に移動して、新規で記事を作成します。
さきほどContent modelとして設定した項目に入力し、テスト記事を公開しましょう。

注意点として、ここで記事がDraftになっていたりすると、アプリ側で記事を取得できないので、確実に最低一記事は公開しておきましょう。
1-5. APIキーの取得
アプリ側からAPIで取得するための鍵情報を取得します。
ヘッダー「Settings」から「API keys」に進み、「Add API key」をクリックします。
名前はなんでもいいのですが、このページでSpace IDとContent Delivery API - access tokenをメモしておいてください。
2. Gatsbyアプリの立ち上げ
今回アプリはGatsby.jsで立ち上げます。すでにContentfulと連携済みのgatsbyアプリがある場合はこのセクションは飛ばしてください。
2-1. Gatsbyのコマンドラインツールをインストール
$ npm install -g gatsby-cli
2-2. アプリを作成するディレクトリに移動して、テストサイトを作成
$ gatsby new hello-world
new
は新しいGatsbyのアプリを作成するコマンドで、これでカレントディレクトリ直下にhello-worldというフォルダが作られます。
2-3. まずはアプリを起動してみる
$ cd hello-world
$ gatsby develop
これでDEVサーバーが起動されるので、http://localhost:8000/ にアクセスしてみます。

成功しました、これでまずGatsbyのベースは作ることができたのでいったんCtrl+Cで終了します。
2-4. 必要なプラグインをインストール
$ npm install --save gatsby-source-contentful@3.1.1 dotenv gatsby-plugin-s3 @contentful/rich-text-react-renderer
- gatsby-source-contentful...Contentfulのデータを扱う。後述のリッチテキスト出力のためにバージョン3をインストール
- dotenv...環境変数を扱えるようになる
- gatsby-plugin-s3...s3にビルド&デプロイする
- @contentful/rich-text-react-renderer...Contentfulのリッチテキストを出力する
次にContentfulとの連携を行います。
2-5. 必要なプラグインをインストール
gatsby-config.js
plugins: [
...
{
resolve: `gatsby-source-contentful`,
options: {
spaceId: process.env.CONTENTFUL_SPACE_ID,
accessToken: process.env.CONTENTFUL_ACCESS_TOKEN,
},
},
],
Contentfulで発行されるSpace IDやAccess Tokenを書いておく必要がありますが、次に書くように環境変数として管理します。
2-6. 環境変数の設定
ルートディレクトリに.env
ファイルを作成し、先ほどメモしたContentfulの鍵情報を記載します。
.env
CONTENTFUL_SPACE_ID=123456789(自分のSpace ID)
CONTENTFUL_ACCESS_TOKEN=abcdefghi(自分のToken)
またgatsby-config.jsに戻って、一番上の行に書き足します。
gatsby-config.js
require('dotenv').config();
module.exports = {
...
2-7. ビルド時のコンテンツ取得設定
ルートにあるgatsby-node.js
の修正して、Contentfulから記事を取得してアプリをビルドできるようにします。
gatsby-node.js
const path = require(`path`);
const slash = require(`slash`);
exports.createPages = ({ graphql, actions }) => {
const { createPage } = actions;
// we use the provided allContentfulBlogArticle query to fetch the data from Contentful
return graphql(
`
{
allContentfulBlogArticle {
edges {
node {
id
slug
}
}
}
}
`
).then(result => {
if (result.errors) {
console.log("Error retrieving contentful data", result.errors);
}
// Resolve the paths to our template
const blogArticleTemplate = path.resolve("./src/templates/post.js");
// Then for each result we create a page.
result.data.allContentfulBlogArticle.edges.forEach(edge => {
createPage({
path: `/post/${edge.node.slug}/`,
component: slash(blogArticleTemplate),
context: {
slug: edge.node.slug,
id: edge.node.id
}
});
});
})
.catch(error => {
console.log("Error retrieving contentful data", error);
});
};
ContentfulはGraphQLをサポートしていますが、
allContentful{コンテンツモデル名}
に対してクエリを書くと、特定のコンテンツモデルの記事を全て取得してくれます。
BlogArticleの部分の名前はContentfulで作成したCONTENT TYPE IDによって変わりますが、GraphQL投げる際は最初も大文字にする点にご注意ください。

2-8. index.jsの編集
GatsbyアプリのTOPページで記事の一覧を表示するようにします。
src/pages/index.js
import React from "react"
import { Link } from "gatsby"
import Layout from "../components/layout"
import Img from "gatsby-image";
import SEO from "../components/seo"
const IndexPage = ({ data, location }) => {
const blogArticles = data.allContentfulBlogArticle.edges;
return (
<Layout>
<SEO title="Home" />
<div className="bg-gray">
<div className="ast-container ast-container-top">
<h2>Blog</h2>
{blogArticles && blogArticles.map(({ node: post }) => {
return (
<Link to={`/post/${post.slug}/`} className="post-basic-item flex-column">
<div className="flex-column-3">
{post.thumbnail && //もしサムネイル画像をもっていれば
<Img
fluid={post.thumbnail.fluid}
className="thumbnail"
/>
}
</div>
<div className="flex-column-9">
<h3>{post.title}</h3>
<div className="post-basic-postedat">Posted on {post.createdAt}</div>
</div>
</Link>
)
})}
</div>
</div>
</Layout>
);
};
export default IndexPage
export const query = graphql`
query QueryTop {
allContentfulBlogArticle: allContentfulBlogArticle( sort: {fields: createdAt, order: DESC}) {
edges {
node {
title
slug
thumbnail {
fluid(maxWidth : 600) {
...GatsbyContentfulFluid_withWebp
}
}
createdAt(formatString: "YYYY-MM-DD")
}
}
}
}
`;
2-9. 記事詳細ページのテンプレート作成
srcの直下にtemplatesというフォルダを作り、その中でpost.jsを作成します。
src/templates/post.js
import React from "react";
import { Link, graphql } from "gatsby";
import Layout from "../components/layout";
import SEO from "../components/seo";
import { BLOCKS } from "@contentful/rich-text-types"
import { documentToReactComponents } from '@contentful/rich-text-react-renderer';
import Img from "gatsby-image";
const options = {
renderText: text => {
return text.split('\n').reduce((children, textSegment, index) => {
return [...children, index > 0 && <br key={index} />, textSegment];
}, []);
},
renderNode: {
[BLOCKS.EMBEDDED_ASSET]: (node) => (
<img
src={node.data.target.fields.file["en-US"].url}
/>
)
},
};
const blogArticle = ({ data, location }) => {
const { title, content, createdAt, thumbnail } = data.contentfulBlogArticle;
return (
<Layout>
<SEO title="post" />
<div className="ast-container ast-container-post">
<div className="main">
<div className="post">
<h1>{title}</h1>
<p className="post__date">Posted on {createdAt}</p>
<div>
{thumbnail && //もしサムネイル画像をもっていれば
<Img
fluid={thumbnail.fluid}
className="thumbnail"
/>
}
</div>
<div className="body-text">
{documentToReactComponents(content.json, options)}
</div>
<p className="post__date">Posted on {createdAt}</p>
</div>
</div>
</div>
</Layout>
);
};
export default blogArticle;
export const pageQuery = graphql`
query( $slug: String) {
contentfulBlogArticle(slug: { eq: $slug }) {
title
content{
json
}
thumbnail {
fluid(maxWidth : 600) {
...GatsbyContentfulFluid_withWebp
}
}
createdAt(formatString: "YYYY-MM-DD")
}
}
`;
2-10. アプリの確認
これでローカルブログアプリができたはずなので、再度$ gatsby develop
して http://localhost:8000/ にアクセスします。

Styleは全く当てていませんが、Contentfulと連結した簡単なブログがローカルに立ち上がりました!
3. S3へのアップロード
先ほどローカルに立ち上げたGatsbyアプリを、AWSのS3バケットでホストしたいと思います。これについては既に記事をアップしているので下記をご参照ください。
上記が完了すると、S3のパブリックなURLで記事を確認できるようになります。

4. CircleCIのセットアップ
CircleCIを使って、上記のビルド&デプロイ処理を自動化したいと思います。
※事前にGithubにリポジトリを作り、先ほどのローカルアプリをmasterブランチにpushしておいてください。
4-1. CircleCIに登録
登録し、Githubとの連携を済ませます。
4-2. プロジェクトのセットアップ
左サイドバーからProject一覧に進み、「Set Up Project」をクリック

「Add config」をクリック

こんな画面に進みます。一回Statusが「Failed」になりますが気にしなくてOKです。

4-3. CircleCI用の環境変数セットアップ
CircleCIにビルド&デプロイを行ってもらうため、必要な環境変数を設定します。
「Project Settings」をクリック

環境変数を追加します。

下記4つの変数を追加してください。
- AWS_ACCESS_KEY_ID
- AWS_SECRET_ACCESS_KEY
- CONTENTFUL_ACCESS_TOKEN
- CONTENTFUL_SPACE_ID
こんな感じになればOK。

最初の2つは「3. S3へのアップロード」で利用したAWSアカウントと同じものを利用します。3と4は先ほど.env
で設定した値を同様のものを入力します。
4-4. CircleCI用のconfigファイル作成
ローカルのアプリのルートディレクトリに .circleci/config.yml を作成します。
.circleci/config.yml
version: 2.1
jobs:
setup:
docker:
- image: circleci/node:10.15.3
working_directory: ~/application
steps:
- checkout
- run:
name: Update npm
command: "sudo npm install -g npm@latest"
- restore_cache:
key: dependency-cache-{{ checksum "package.json" }}
- run:
name: Install npm wee
command: npm install
- save_cache:
key: dependency-cache-{{ checksum "package.json" }}
paths:
- node_modules
- save_cache:
key: v1-repo-{{ .Environment.CIRCLE_SHA1 }}
paths:
- ~/application
build:
docker:
- image: circleci/node:10.15.3
working_directory: ~/application
steps:
- restore_cache:
key: v1-repo-{{ .Environment.CIRCLE_SHA1 }}
- restore_cache:
key: dependency-cache-{{ checksum "package.json" }}
- run:
name: Deploy
command: |
npm run build && npm run deploy
workflows:
version: 2.1
build:
jobs:
- setup
- build:
requires:
- setup
filters:
branches:
only:
- master
4-5. デプロイ
再度リモートのmasterブランチにpushしてみます。
CircleCIではビルド&デプロイ状況がリアルタイムで確認できます。
setupとbuildが成功していればOKです。

先ほど立ち上げたS3のURLにアクセスすると、更新されたサイトが確認できるかと思います。
これでGithub上のmasterブランチに変更があった場合に、自動でビルド&デプロイが行われるようになりました。
5. ContentfulとCircleCIの連携
最後に、コンテンツ管理を行うContentfulでの変更もトリガーにします。
5-1. ContentfulのAPIキーの変数をCircleCIに登録
.envの環境変数をCircleCIから使えるように、.circleci/config.yml
を修正して、ビルド処理の直前に変数を取得する処理を追加します。
.circleci/config.yml
...
- run:
name: Deploy
command: |
echo CONTENTFUL_SPACE_ID=${CONTENTFUL_SPACE_ID} > ~/.env
echo CONTENTFUL_ACCESS_TOKEN=${CONTENTFUL_ACCESS_TOKEN} >> ~/.env
npm run build && npm run deploy
...
5-2.ContentfulでCircleCIプラグインをインストール
https://www.contentful.com/marketplace/webhook/circle-ci/ にアクセスしてインストールします。

基本的にはContentfulのインストラクションに沿ってインストールするだけです。
先ほどContentfulで作成したSpaceを選択するようご注意ください。

Github情報を入力します。
※Personal API TokenはCircleCIのダッシュボードから取得します

ContentfulのWebhook設定ページにリダイレクトするので、設定を若干修正してみます。
先ほどContentfulで作成した「Blog Article」のみを対象にしたのと、記事が保存された際もビルド処理が走るようにします。

リポジトリがpublicの場合、これで準備は完了です。
適当に記事を更新すると、CircleCIのビルドが走るチェックしてみてください。
先程と同様に「Success」になっていれば大丈夫です。

さいごに
今回の記事で継続的な開発環境の基本は抑えていると思いますが、次のステップとしては下記でしょうか。
- CloudinaryをContentfulに連携し、画像管理&配信を外部化する
- Cloudfrontを導入してより高速なサイト表示
ビルド&デプロイ&ホスティングを行うNetlifyなどのサービスもありますが、月ごとのビルド処理が一定時間を超えると課金されてしまいますし、Basic認証の利用が難しかったり日本にエッジポイントがなかったりするので、自分で開発環境を構築しておいたほうが後々柔軟に対応できると思います。
CircleCI、Contentfulのご利用をお考えの方がいましたら、ぜひお気軽に弊社にお問い合わせください。